Нужна проверить тестирование изменений, связанных с внедрением улучшенной рекомендательной системы. Оцените корректность проведения теста и проанализируйте результаты теста.
Среди данных у нас есть идентификаторы пользователя, тип события, дата и время события, дата регистрации, регион пользователя и устройство регистрации, таблица участников тестов и календарь маркетинговых событий на 2020 год. Данные разбиты на две группы A и B.
import pandas as pd
import datetime as dt
import numpy as np
import matplotlib.pyplot as plt
from plotly import graph_objects as go
from scipy import stats as st
import math as mth
import warnings
import seaborn as sns
Загрузим данные действий новых пользователей в период с 7 декабря 2020 по 4 января 2021 года.
try:
events = pd.read_csv('/datasets/final_ab_events.csv')
except:
events = pd.read_csv('C:\\Users\\User\\Desktop\\fin_pr\\final_ab_events.csv')
events.head()
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | purchase | 99.99 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | purchase | 9.99 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | purchase | 4.99 |
| 3 | 96F27A054B191457 | 2020-12-07 04:02:40 | purchase | 4.99 |
| 4 | 1FD7660FDF94CA1F | 2020-12-07 10:15:09 | purchase | 4.99 |
Загрузим календарь маркетинговых событий на 2020 год.
try:
marketing_events = pd.read_csv('/datasets/ab_project_marketing_events.csv')
except:
marketing_events = pd.read_csv('C:\\Users\\User\\Desktop\\fin_pr\\ab_project_marketing_events.csv')
marketing_events.head()
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
Пользователи, зарегистрировавшиеся с 7 по 21 декабря 2020 года.
try:
new_users = pd.read_csv('/datasets/final_ab_new_users.csv')
except:
new_users = pd.read_csv('C:\\Users\\User\\Desktop\\fin_pr\\final_ab_new_users.csv')
new_users.head()
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | PC |
| 1 | F1C668619DFE6E65 | 2020-12-07 | N.America | Android |
| 2 | 2E1BF1D4C37EA01F | 2020-12-07 | EU | PC |
| 3 | 50734A22C0C63768 | 2020-12-07 | EU | iPhone |
| 4 | E1BDDCE0DAFA2679 | 2020-12-07 | N.America | iPhone |
Таблица участников тестов.
try:
participants = pd.read_csv('/datasets/final_ab_participants.csv')
except:
participants = pd.read_csv('C:\\Users\\User\\Desktop\\fin_pr\\final_ab_participants.csv')
participants.head()
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
Название столбцов коректны, нужна проверить типы данных, следует изучить наличие пропусков и дубликатов.
Посмотрим на наличие дубликатов и пропусков.
events.info();
<class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null object 2 event_name 440317 non-null object 3 details 62740 non-null float64 dtypes: float64(1), object(3) memory usage: 13.4+ MB
print(events.isna().sum())
print('Количество дубликатов:', events.duplicated().sum());
user_id 0 event_dt 0 event_name 0 details 377577 dtype: int64 Количество дубликатов: 0
Есть пропуски в details, но они связаны с тем что не все события имеют дополнительные данные. Они не помешают нашему анализу оставим их как есть.
marketing_events.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null object 3 finish_dt 14 non-null object dtypes: object(4) memory usage: 576.0+ bytes
print(marketing_events.isna().sum())
print('Количество дубликатов:', marketing_events.duplicated().sum())
name 0 regions 0 start_dt 0 finish_dt 0 dtype: int64 Количество дубликатов: 0
new_users.info()
new_users['user_id'].nunique()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null object 2 region 61733 non-null object 3 device 61733 non-null object dtypes: object(4) memory usage: 1.9+ MB
61733
print(new_users.isna().sum())
print('Количество дубликатов:', new_users.duplicated().sum())
new_users['user_id'].nunique()
user_id 0 first_date 0 region 0 device 0 dtype: int64 Количество дубликатов: 0
61733
participants.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 18268 entries, 0 to 18267 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 18268 non-null object 1 group 18268 non-null object 2 ab_test 18268 non-null object dtypes: object(3) memory usage: 428.3+ KB
print(participants.isna().sum())
print('Количество дубликатов:', participants.duplicated().sum())
user_id 0 group 0 ab_test 0 dtype: int64 Количество дубликатов: 0
Проверим на неявные дубликаты
new_users['user_id'].value_counts().head()
BC1E96104DDE433A 1 D45554BA350E10C4 1 CC311F9ED000F25E 1 CA84538479857DBD 1 7F59E027E41EB119 1 Name: user_id, dtype: int64
participants['user_id'].value_counts().head()
9CBD8387C8A1DDDF 2 4D269D6E438C6D22 2 B70E5E2275EEAA7F 2 06C6018D3CB3E903 2 87314190D7FC4E12 2 Name: user_id, dtype: int64
Есть дубликаты, видимо пользователи пересекаются, отфильтруем их на следующем этапе.
Поменяем типы данных.
events['event_dt'] = pd.to_datetime(
events['event_dt'] , format='%Y-%m-%dT%H:%M:%S'
)
events.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null datetime64[ns] 2 event_name 440317 non-null object 3 details 62740 non-null float64 dtypes: datetime64[ns](1), float64(1), object(2) memory usage: 13.4+ MB
marketing_events['start_dt'] = marketing_events['start_dt'].map(
lambda x: dt.datetime.strptime(x, '%Y-%m-%d')
)
marketing_events['finish_dt'] = marketing_events['finish_dt'].map(
lambda x: dt.datetime.strptime(x, '%Y-%m-%d')
)
marketing_events.info();
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null datetime64[ns] 3 finish_dt 14 non-null datetime64[ns] dtypes: datetime64[ns](2), object(2) memory usage: 576.0+ bytes
new_users['first_date'] = new_users['first_date'].map(
lambda x: dt.datetime.strptime(x, '%Y-%m-%d')
)
new_users.info();
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null datetime64[ns] 2 region 61733 non-null object 3 device 61733 non-null object dtypes: datetime64[ns](1), object(3) memory usage: 1.9+ MB
У столбцов с датами изменены типы на более подходящие, пропусков нет дубликаты нет.
Проревем корректность всех пунктов технического задания.
recommender_system_test;print(participants.groupby(['ab_test', 'group']).count())
user_id
ab_test group
interface_eu_test A 5831
B 5736
recommender_system_test A 3824
B 2877
У нас есть два теста, среди них есть recommender_system_test и есть группы А — контрольная, B — новая платёжная воронка.
Проверим;
print('Дата запуска:', new_users['first_date'].min())
Дата запуска: 2020-12-07 00:00:00
Дата запуска корректна.
print('Дата остановки набора новых пользователей:', new_users['first_date'].max())
Дата остановки набора новых пользователей: 2020-12-23 00:00:00
Дата остановки чуть позже на пару дней, возможно эта связано со вторым тестом который шёл параллельно. У нас есть нужная нам число 2020-12-21.
print('Дата остановки:', events['event_dt'].max())
Дата остановки: 2020-12-30 23:36:33
Тест остановился раньше положеного на пять дней.
Аудитория: 15% новых пользователей из региона EU;
users_data = new_users.pivot_table(index='region', values='user_id', aggfunc='nunique')
count = new_users['user_id'].nunique()
users_data['user_%'] = (users_data['user_id'] / count) * 100
users_data.reset_index()
| region | user_id | user_% | |
|---|---|---|---|
| 0 | APAC | 3153 | 5.107479 |
| 1 | CIS | 3155 | 5.110719 |
| 2 | EU | 46270 | 74.951809 |
| 3 | N.America | 9155 | 14.829994 |
users_data.plot(kind='pie', x='region', y='user_%',
figsize=(15, 10),
autopct='%1.1f%%',
shadow=True)
plt.legend(loc=8, fontsize=10)
plt.title('Аудитория новых пользователей по регионам')
plt.show()
75% пользователей из региона EU;
new_users = new_users.loc[(new_users['first_date'] <= '2020-12-21')]
new_eu_users = new_users.query('region == "EU"')
new_eu_users
new_eu_users.head()
new_eu_users['user_id'].nunique()
42340
user_all = new_users.query('user_id in @participants.user_id')
print(user_all.nunique())
#new_eu_users = new_eu_users.query('user_id in @test_group_eu.user_id')
#new_eu_users.count()
#print(participants.groupby(['ab_test', 'group']).count())
user_id 15664 first_date 15 region 4 device 4 dtype: int64
Оставим нужных нам пользователей.
#yu = new_users.query('user_id in @participants.user_id')
new_eu_users.nunique()
print(5532/42340*100)
13.065658951346245
Фактически 13% новых пользователей из региона EU.
Удалим пересекающихся пользователей которые попали в два теста одновременно.
t1 = participants.query('ab_test == "recommender_system_test"')
t2 = participants.query('ab_test != "recommender_system_test" and group != "B"')
#df.query('Price <= @maximum_price')
test_group = t1.query('user_id not in @t2.user_id')
print(test_group.groupby(['ab_test', 'group']).count())
test_group['user_id'].nunique()
user_id
ab_test group
recommender_system_test A 3342
B 2540
5882
Оставляем пользователей только из eu региона.
test_group_eu = test_group.query('user_id in @new_eu_users.user_id')
test_group_eu['user_id'].nunique()
5532
Ожидаемое количество участников теста 6000 после удаления пересикающихся пользователей 5532.
events_s = events.query('user_id in @new_eu_users.user_id')
events_s = events.query('user_id in @test_group_eu.user_id')
print(events_s.head())
user_id event_dt event_name details 5 831887FE7F2D6CBA 2020-12-07 06:50:29 purchase 4.99 17 3C5DD0288AC4FE23 2020-12-07 19:42:40 purchase 4.99 58 49EA242586C87836 2020-12-07 06:31:24 purchase 99.99 74 A640F31CAC7823A6 2020-12-07 18:48:26 purchase 4.99 93 2F46396B6766CFDB 2020-12-07 13:29:30 purchase 4.99
print(events_s.count())
print(events_s['user_id'].nunique())
user_id 20382 event_dt 20382 event_name 20382 details 2740 dtype: int64 3025
Ожидаемый эффект: за 14 дней с момента регистрации пользователи покажут улучшение каждой метрики не менее, чем на 10%:
product_page,product_cart,purchase.Для ожидаемого эффект нам нужно оставить клиентов прожившие лайфтайм 14 дней. Удалим события которые находятся за пределами 2020-12-16.
ad_costs = test_group_eu.merge(new_eu_users, on=['user_id'], how='left')
ad_data = events_s.merge(ad_costs, on=['user_id'], how='left')
ad_data['event_day'] = pd.to_datetime(ad_data['event_dt']).dt.normalize();
ad_data.head()
#ad_data = ad_data.loc[(ad_data['first_date'] <= '2020-12-16')]
| user_id | event_dt | event_name | details | group | ab_test | first_date | region | device | event_day | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 831887FE7F2D6CBA | 2020-12-07 06:50:29 | purchase | 4.99 | A | recommender_system_test | 2020-12-07 | EU | Android | 2020-12-07 |
| 1 | 3C5DD0288AC4FE23 | 2020-12-07 19:42:40 | purchase | 4.99 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-07 |
| 2 | 49EA242586C87836 | 2020-12-07 06:31:24 | purchase | 99.99 | B | recommender_system_test | 2020-12-07 | EU | iPhone | 2020-12-07 |
| 3 | A640F31CAC7823A6 | 2020-12-07 18:48:26 | purchase | 4.99 | B | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-07 |
| 4 | 2F46396B6766CFDB | 2020-12-07 13:29:30 | purchase | 4.99 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-07 |
ad_data['lifetime'] = (
ad_data['event_day'] - ad_data['first_date']
).dt.days
#ad_data = ad_data.loc[(ad_data['lifetime'] == 14)]
ad_data = ad_data.query('lifetime < 14 ')
ad_data
| user_id | event_dt | event_name | details | group | ab_test | first_date | region | device | event_day | lifetime | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 831887FE7F2D6CBA | 2020-12-07 06:50:29 | purchase | 4.99 | A | recommender_system_test | 2020-12-07 | EU | Android | 2020-12-07 | 0 |
| 1 | 3C5DD0288AC4FE23 | 2020-12-07 19:42:40 | purchase | 4.99 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-07 | 0 |
| 2 | 49EA242586C87836 | 2020-12-07 06:31:24 | purchase | 99.99 | B | recommender_system_test | 2020-12-07 | EU | iPhone | 2020-12-07 | 0 |
| 3 | A640F31CAC7823A6 | 2020-12-07 18:48:26 | purchase | 4.99 | B | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-07 | 0 |
| 4 | 2F46396B6766CFDB | 2020-12-07 13:29:30 | purchase | 4.99 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-07 | 0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 20376 | 930EACAE048DFF45 | 2020-12-29 06:56:00 | login | NaN | A | recommender_system_test | 2020-12-20 | EU | PC | 2020-12-29 | 9 |
| 20377 | E5589EAE02ACD150 | 2020-12-29 22:17:08 | login | NaN | A | recommender_system_test | 2020-12-20 | EU | Mac | 2020-12-29 | 9 |
| 20378 | D21F0D4FDCD82DB2 | 2020-12-29 02:17:00 | login | NaN | A | recommender_system_test | 2020-12-20 | EU | iPhone | 2020-12-29 | 9 |
| 20379 | 96BDD55846D1F7F6 | 2020-12-29 16:53:42 | login | NaN | A | recommender_system_test | 2020-12-20 | EU | iPhone | 2020-12-29 | 9 |
| 20380 | 553BAE96C6EB6240 | 2020-12-29 14:09:14 | login | NaN | A | recommender_system_test | 2020-12-20 | EU | Android | 2020-12-29 | 9 |
19689 rows × 11 columns
ad_data['user_id'].nunique()
3025
#events_count = events_s.groupby('event_name').agg({'user_id': 'count'})
events_count = (ad_data.groupby('event_name')
.agg({'user_id': 'nunique'})
.sort_values(by='user_id', ascending=False)
.reset_index()
)
events_count = events_count.iloc[[0,1,3,2]]
events_count
| event_name | user_id | |
|---|---|---|
| 0 | login | 3024 |
| 1 | product_page | 1905 |
| 3 | product_cart | 899 |
| 2 | purchase | 933 |
fig = go.Figure(
go.Funnel(
y=[
'Авторизовались',
'Просмотр карточек товаров',
'Просмотры корзины',
'Покупки',
],
x=[3024, 1905, 899, 933],
)
)
fig.show()
За 14 дней с момента регистрации улучшение каждой метрики более 10%. В конце воронки покупок больше чем в пред идущем событии эта скорее всего из за покупок в один клик.
Итог соответствия данных техническому заданию:
recommender_system_test;product_page равняется 64%,product_cart равняется 47%,purchase равняется 101%.Убедимся, что время проведения теста не совпадает с маркетинговыми и другими активностями.
marketing_events
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
| 5 | Black Friday Ads Campaign | EU, CIS, APAC, N.America | 2020-11-26 | 2020-12-01 |
| 6 | Chinese New Year Promo | APAC | 2020-01-25 | 2020-02-07 |
| 7 | Labor day (May 1st) Ads Campaign | EU, CIS, APAC | 2020-05-01 | 2020-05-03 |
| 8 | International Women's Day Promo | EU, CIS, APAC | 2020-03-08 | 2020-03-10 |
| 9 | Victory Day CIS (May 9th) Event | CIS | 2020-05-09 | 2020-05-11 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
| 11 | Dragon Boat Festival Giveaway | APAC | 2020-06-25 | 2020-07-01 |
| 12 | Single's Day Gift Promo | APAC | 2020-11-11 | 2020-11-12 |
| 13 | Chinese Moon Festival | APAC | 2020-10-01 | 2020-10-07 |
A = events['event_dt'].max() # начало теста
B = events['event_dt'].min() # конец теста
print(marketing_events.query("start_dt <= @A" and "finish_dt >= @B"))
print(' ')
print('Тест совпадает с маркетинговыми и другими активностям:',
marketing_events.query("start_dt <= @A" and "finish_dt >= @B")['name'].nunique());
name regions start_dt finish_dt 0 Christmas&New Year Promo EU, N.America 2020-12-25 2021-01-03 10 CIS New Year Gift Lottery CIS 2020-12-30 2021-01-07 Тест совпадает с маркетинговыми и другими активностям: 2
Тест совпадает с Christmas&New Year Promo которое начинается с 2020-12-25, также очень близко проходит New Year Gift Lottery (2020-12-30 по 2021-01-07), но он коснется только региона CIS. За неделю до исследование в европейском регионе заканчивается Black Friday Ads Campaign.
Проверим аудиторию теста.
Если хотитим увеличить показатель на 10% с помощью изменения, понадобится выборка минимум из 659 человек.
Проверим на пересечения в тестах.
#participants['user_id'].value_counts().head()
participants['user_id'].duplicated().sum()
1602
test_p = participants.query('ab_test != "recommender_system_test"')
#test_p
old = [test_p['user_id']]
for new in ad_costs['user_id']:
if new == old:
print('Пересечений с конкурирующим тестом есть')
else:
end = 'Пересечений с конкурирующим тестом нет'
print(end)
Пересечений с конкурирующим тестом нет
test_p['user_id'].duplicated().sum()
0
test_group_eu['user_id'].value_counts().head()
CB3289BB00E5E465 1 9C2A5E3BF66CBB97 1 6070727198404A40 1 6BAD4743388F4545 1 EB3589638ED1D779 1 Name: user_id, dtype: int64
Пользователей одновременно участвующих в двух группах теста нет.
a_data = ad_data.query('group == "A"')
b_data = ad_data.query('group == "B"')
a_cr = (a_data.groupby('event_name')
.agg({'user_id': 'nunique'})
.sort_values(by='user_id', ascending=False)
.reset_index()
)
#a_cr
b_cr = (b_data.groupby('event_name')
.agg({'user_id': 'nunique'})
.sort_values(by='user_id', ascending=False)
.reset_index()
)
#b_cr
old_a = (a_data.groupby('first_date')
.agg({'event_name': 'count', 'user_id': 'nunique'})
.sort_values(by='first_date', ascending=False)
.reset_index()
)
old_a
| first_date | event_name | user_id | |
|---|---|---|---|
| 0 | 2020-12-21 | 2257 | 339 |
| 1 | 2020-12-20 | 1470 | 228 |
| 2 | 2020-12-19 | 1342 | 204 |
| 3 | 2020-12-18 | 1441 | 184 |
| 4 | 2020-12-17 | 1269 | 172 |
| 5 | 2020-12-16 | 1046 | 143 |
| 6 | 2020-12-15 | 1346 | 170 |
| 7 | 2020-12-14 | 2414 | 316 |
| 8 | 2020-12-13 | 212 | 47 |
| 9 | 2020-12-12 | 250 | 51 |
| 10 | 2020-12-11 | 443 | 79 |
| 11 | 2020-12-10 | 263 | 54 |
| 12 | 2020-12-09 | 414 | 68 |
| 13 | 2020-12-08 | 504 | 74 |
| 14 | 2020-12-07 | 812 | 135 |
new_b = (b_data.groupby('first_date')
.agg({'event_name': 'count', 'user_id': 'nunique'})
.sort_values(by='first_date', ascending=False)
.reset_index()
)
new_b
| first_date | event_name | user_id | |
|---|---|---|---|
| 0 | 2020-12-21 | 328 | 68 |
| 1 | 2020-12-20 | 274 | 53 |
| 2 | 2020-12-19 | 196 | 39 |
| 3 | 2020-12-18 | 223 | 44 |
| 4 | 2020-12-17 | 163 | 33 |
| 5 | 2020-12-16 | 627 | 85 |
| 6 | 2020-12-15 | 123 | 27 |
| 7 | 2020-12-14 | 290 | 60 |
| 8 | 2020-12-13 | 34 | 12 |
| 9 | 2020-12-12 | 222 | 45 |
| 10 | 2020-12-11 | 48 | 15 |
| 11 | 2020-12-10 | 129 | 29 |
| 12 | 2020-12-09 | 379 | 66 |
| 13 | 2020-12-08 | 194 | 37 |
| 14 | 2020-12-07 | 976 | 148 |
Проверем равномерность распределения по тестовым группам и правильность их формирования.
Сформулируем гипотезы:
#from scipy import stats as st
old = old_a['user_id']
new = new_b['user_id']
alpha = 0.05
results = st.mannwhitneyu(old, new, True, 'less')
print('p-значение: ', results.pvalue)
if results.pvalue < alpha:
print('Отвергаем нулевую гипотезу: разница статистически значима')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, вывод о различии сделать нельзя'
)
p-значение: 0.9998581436703973 Не получилось отвергнуть нулевую гипотезу, вывод о различии сделать нельзя
sns.set_style('whitegrid')
# назначаем размер графика
plt.figure(figsize=(10, 4))
# строим линейный график средствами seaborn
sns.lineplot(x='first_date', y='event_name', data=old_a, marker='D', label = 'group А')
sns.lineplot(x='first_date', y='event_name', data=new_b, marker='D', label = 'group B')
# формируем заголовок графика и подписи осей средствами matplotlib
plt.title('График распределения по тестовым группам ')
plt.xlabel('Дата')
plt.ylabel('Количество')# отображаем график на экране
plt.show()
Большой скачек с 13 числа в группе А возможна эта из за маркетинговых событий, однако у группы В таково скачка нет.
ax_data = ad_data.query('group == "A"')
bx_data = ad_data.query('group == "B"')
ax_data['user_id'].nunique()
2264
bx_data['user_id'].nunique()
761
id_event = (
ax_data.groupby('user_id')
.agg({'event_name' : 'count'})
.sort_values(by='event_name', ascending=False)
)
fig, ax = plt.subplots()
id_event['event_name'].hist(figsize=(8, 5), bins=(25))
ax.set_title('Количество событий на пользователя группа А')
ax.set_xlabel('Количество событий')
ax.set_ylabel('Количество пользователей')
plt.show();
display(id_event.reset_index().head(10))
print('Медиана событий на пользователя групп А:',id_event['event_name'].median())
| user_id | event_name | |
|---|---|---|
| 0 | CED71698585A2E46 | 24 |
| 1 | B8EF6F0325A9979F | 21 |
| 2 | A25712EE46AD443A | 20 |
| 3 | 97AD409895906A32 | 20 |
| 4 | 7347C03E6A300EFD | 20 |
| 5 | 2F0639CBF0C3C249 | 20 |
| 6 | 109FE65EE47113C9 | 20 |
| 7 | 19F5032292917412 | 20 |
| 8 | 77FC0E20AEAC1506 | 20 |
| 9 | 1BFEE479308EFF44 | 20 |
Медиана событий на пользователя групп А: 6.0
id_event = (
bx_data.groupby('user_id')
.agg({'event_name' : 'count'})
.sort_values(by='event_name', ascending=False)
)
fig, ax = plt.subplots()
id_event['event_name'].hist(figsize=(8, 5), bins=(25))
ax.set_title('Количество событий на пользователя группа B')
ax.set_xlabel('Количество событий')
ax.set_ylabel('Количество пользователей')
plt.show();
display(id_event.reset_index().head(10))
print('Медиана событий на пользователя групп B:',id_event['event_name'].median())
| user_id | event_name | |
|---|---|---|
| 0 | 1198061F6AF34B7B | 24 |
| 1 | 115EBC1CA027854A | 21 |
| 2 | 89545C7F903DBA34 | 21 |
| 3 | 7E8720DB6A21CF66 | 20 |
| 4 | 2C2BE85372033F77 | 20 |
| 5 | C8460FF8BEF553A4 | 18 |
| 6 | 4EFB5E89AC11AC6D | 16 |
| 7 | A9908F62C41613A8 | 16 |
| 8 | 37094134968B2013 | 16 |
| 9 | FE76759FE6BF8C68 | 16 |
Медиана событий на пользователя групп B: 4.0
У групп разброс разный, у группы В разброс сильнее смещен в лево на меньшие количество событий чем у группы А.
day_a = ax_data.groupby('event_day').agg({'event_name': 'count'})
day_b = bx_data.groupby('event_day').agg({'event_name': 'count'})
day_a
| event_name | |
|---|---|
| event_day | |
| 2020-12-07 | 276 |
| 2020-12-08 | 269 |
| 2020-12-09 | 322 |
| 2020-12-10 | 283 |
| 2020-12-11 | 311 |
| 2020-12-12 | 300 |
| 2020-12-13 | 268 |
| 2020-12-14 | 890 |
| 2020-12-15 | 895 |
| 2020-12-16 | 885 |
| 2020-12-17 | 1029 |
| 2020-12-18 | 1067 |
| 2020-12-19 | 1272 |
| 2020-12-20 | 1235 |
| 2020-12-21 | 1640 |
| 2020-12-22 | 1056 |
| 2020-12-23 | 824 |
| 2020-12-24 | 693 |
| 2020-12-25 | 518 |
| 2020-12-26 | 458 |
| 2020-12-27 | 450 |
| 2020-12-28 | 303 |
| 2020-12-29 | 239 |
day_b
| event_name | |
|---|---|
| event_day | |
| 2020-12-07 | 309 |
| 2020-12-08 | 200 |
| 2020-12-09 | 299 |
| 2020-12-10 | 213 |
| 2020-12-11 | 139 |
| 2020-12-12 | 180 |
| 2020-12-13 | 145 |
| 2020-12-14 | 214 |
| 2020-12-15 | 199 |
| 2020-12-16 | 322 |
| 2020-12-17 | 245 |
| 2020-12-18 | 235 |
| 2020-12-19 | 265 |
| 2020-12-20 | 274 |
| 2020-12-21 | 331 |
| 2020-12-22 | 147 |
| 2020-12-23 | 130 |
| 2020-12-24 | 105 |
| 2020-12-25 | 58 |
| 2020-12-26 | 55 |
| 2020-12-27 | 56 |
| 2020-12-28 | 50 |
| 2020-12-29 | 35 |
sns.set_style('whitegrid')
# назначаем размер графика
plt.figure(figsize=(10, 4))
# строим линейный график средствами seaborn
sns.lineplot(x='event_day', y='event_name', data=day_a, marker='D', label = 'group А')
sns.lineplot(x='event_day', y='event_name', data=day_b, marker='D', label = 'group B')
# формируем заголовок графика и подписи осей средствами matplotlib
plt.title('График распределения событий по дням')
plt.xlabel('Дата')
plt.ylabel('Количество')# отображаем график на экране
plt.show()
По какой то причини в группе А очень большой скачек клиентов и совершаемыми ими события с 13.12 по 21.12 число, возможно это влияние приближающегося рождества, в группе В такого аномального скачка не наблюдается.
Посмотрим на распределения по девайсам в группах.
dev_a = ax_data.groupby('device').agg({'event_name': 'count', 'user_id': 'nunique'})
sum_a = dev_a['event_name'].sum()
dev_a['event_%'] = (dev_a['event_name'] / sum_a) * 100
dev_b = bx_data.groupby('device').agg({'event_name': 'count', 'user_id': 'nunique'})
sum_b = dev_b['event_name'].sum()
dev_b['event_%'] = (dev_b['event_name'] / sum_b) * 100
sum_a = dev_a['user_id'].sum()
dev_a['user_%'] = (dev_a['user_id'] / sum_a) * 100
sum_b = dev_b['user_id'].sum()
dev_b['user_%'] = (dev_b['user_id'] / sum_b) * 100
dev_a.plot(kind='pie', x='device', y='event_%',
figsize=(15, 10),
autopct='%1.1f%%',
shadow=True)
plt.legend(loc=8, fontsize=10)
plt.title('Аудитория новых пользователей по девайсам А')
plt.show()
dev_a.reset_index()
| device | event_name | user_id | event_% | user_% | |
|---|---|---|---|---|---|
| 0 | Android | 6707 | 994 | 43.318478 | 43.904594 |
| 1 | Mac | 1558 | 221 | 10.062649 | 9.761484 |
| 2 | PC | 4076 | 600 | 26.325647 | 26.501767 |
| 3 | iPhone | 3142 | 449 | 20.293225 | 19.832155 |
dev_b.plot(kind='pie', x='device', y='event_%',
figsize=(15, 10),
autopct='%1.1f%%',
shadow=True)
plt.legend(loc=8, fontsize=10)
plt.title('Аудитория новых пользователей по девайсам В')
plt.show()
dev_b.reset_index()
| device | event_name | user_id | event_% | user_% | |
|---|---|---|---|---|---|
| 0 | Android | 1959 | 354 | 46.576320 | 46.517740 |
| 1 | Mac | 340 | 65 | 8.083690 | 8.541393 |
| 2 | PC | 984 | 185 | 23.395150 | 24.310118 |
| 3 | iPhone | 923 | 157 | 21.944841 | 20.630749 |
В группе В пользователей на мобильных устройствах больше примерно на 3%.
a_v = (ax_data.groupby('event_name')
.agg({'user_id': 'nunique'})
.sort_values(by='user_id', ascending=False)
.reset_index()
)
a_v = a_v.iloc[[0,1,3,2]]
display(a_v)
fig = go.Figure(
go.Funnel(
y=[
'Авторизовались',
'Просмотр карточек товаров',
'Просмотры корзины',
'Покупки',
],
x=[2264, 1474, 685, 712],
)
)
fig.show()
| event_name | user_id | |
|---|---|---|
| 0 | login | 2264 |
| 1 | product_page | 1474 |
| 3 | product_cart | 685 |
| 2 | purchase | 712 |
Наибольший отток в группе А приходится между переходом от просмотра карточек товаров к просмотру корзины, переходя на этапе покупок идет прирост. От авторизация до просмотра карточек теряется 36,6%. От авторизации до покупки дошли 31%.
b_v = (bx_data.groupby('event_name')
.agg({'user_id': 'nunique'})
.sort_values(by='user_id', ascending=False)
.reset_index()
)
b_v = b_v.iloc[[0,1,3,2]]
display(b_v)
fig = go.Figure(
go.Funnel(
y=[
'Авторизовались',
'Просмотр карточек товаров',
'Просмотры корзины',
'Покупки',
],
x=[760, 431, 214, 221],
)
)
fig.show()
| event_name | user_id | |
|---|---|---|
| 0 | login | 760 |
| 1 | product_page | 431 |
| 3 | product_cart | 214 |
| 2 | purchase | 221 |
В группе В конверсия не такая большая от авторизации до просмотра теряется около 43.7% клиентов на следующем этапе от карточек до корзины 50%, но на последней этапе идет прирост на 3% видимо рекомендации группы B показывают хорошие показатели на быстрых покупках в один клик. От авторизации до покупки у группы В дошли 29% эти показатели немного ниже чем у группы А.
ad_data.head(1)
| user_id | event_dt | event_name | details | group | ab_test | first_date | region | device | event_day | lifetime | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 831887FE7F2D6CBA | 2020-12-07 06:50:29 | purchase | 4.99 | A | recommender_system_test | 2020-12-07 | EU | Android | 2020-12-07 | 0 |
details_a = ax_data.groupby('event_day').agg({'details': 'mean'})
details_b = bx_data.groupby('event_day').agg({'details': 'mean'})
#ax_data['details'].value_counts()
#bx_data['details'].value_counts()
details_a
| details | |
|---|---|
| event_day | |
| 2020-12-07 | 24.434444 |
| 2020-12-08 | 24.490000 |
| 2020-12-09 | 17.199302 |
| 2020-12-10 | 18.147895 |
| 2020-12-11 | 27.704286 |
| 2020-12-12 | 24.490000 |
| 2020-12-13 | 9.990000 |
| 2020-12-14 | 26.373929 |
| 2020-12-15 | 21.455517 |
| 2020-12-16 | 21.697317 |
| 2020-12-17 | 16.874058 |
| 2020-12-18 | 18.133939 |
| 2020-12-19 | 22.392235 |
| 2020-12-20 | 27.630449 |
| 2020-12-21 | 26.400788 |
| 2020-12-22 | 29.450432 |
| 2020-12-23 | 12.665439 |
| 2020-12-24 | 28.626364 |
| 2020-12-25 | 32.284118 |
| 2020-12-26 | 22.760270 |
| 2020-12-27 | 18.990000 |
| 2020-12-28 | 12.431860 |
| 2020-12-29 | 8.538387 |
details_b
| details | |
|---|---|
| event_day | |
| 2020-12-07 | 12.778462 |
| 2020-12-08 | 12.748621 |
| 2020-12-09 | 11.460588 |
| 2020-12-10 | 26.240000 |
| 2020-12-11 | 5.990000 |
| 2020-12-12 | 12.990000 |
| 2020-12-13 | 58.948333 |
| 2020-12-14 | 8.955517 |
| 2020-12-15 | 53.138148 |
| 2020-12-16 | 22.063171 |
| 2020-12-17 | 31.823333 |
| 2020-12-18 | 9.097143 |
| 2020-12-19 | 12.409355 |
| 2020-12-20 | 23.777879 |
| 2020-12-21 | 47.924783 |
| 2020-12-22 | 9.390000 |
| 2020-12-23 | 11.101111 |
| 2020-12-24 | 5.823333 |
| 2020-12-25 | 19.275714 |
| 2020-12-26 | 18.561429 |
| 2020-12-27 | 126.101111 |
| 2020-12-28 | 6.656667 |
| 2020-12-29 | 4.990000 |
sns.set_style('whitegrid')
# назначаем размер графика
plt.figure(figsize=(12, 6))
# строим линейный график средствами seaborn
sns.lineplot(x='event_day', y='details', data=details_a, marker='D', label = 'group А')
sns.lineplot(x='event_day', y='details', data=details_b, marker='D', label = 'group B')
# формируем заголовок графика и подписи осей средствами matplotlib
plt.title('График распределения стоимость покупки по дням')
plt.xlabel('Дата')
plt.ylabel('Количество')# отображаем график на экране
plt.show()
details_a['details'].mean()
21.441783882419678
details_b['details'].mean()
24.01063901839584
В группе В можно видеть сильные скачки по суммам покупок, однака общая средняя у нее меньше чем у группы В, возможна новые рекомендации влияют больше на покупки нежили на привлечение новых клиентов, однако они по какой-та причине не стабильны.
Прежде чем приступать к A/B-тестированию нужно учесть некоторые особенности данных:
По результаты A/В-тестирования можно подвести итоги:
Сформулируем гипотезы:
alpha = 0.05
purchases = np.array([431, 1474])
leads = np.array([760, 2264])
p1 = purchases[0] / leads[0]
p2= purchases[1] / leads[1]
p_combined = (purchases[0] + purchases[1]) / (leads[0] + leads[1])
difference = p1 - p2
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/leads[0] + 1/leads[1]))
distr = st.norm(0, 1)
p_value = (1-distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными'
)
p-значение: 3.3566961849640364e-05 Отвергаем нулевую гипотезу: между долями есть значимая разница
purchases = np.array([214, 685])
leads = np.array([760, 2264])
p1 = purchases[0] / leads[0]
p2= purchases[1] / leads[1]
p_combined = (purchases[0] + purchases[1]) / (leads[0] + leads[1])
difference = p1 - p2
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/leads[0] + 1/leads[1]))
distr = st.norm(0, 1)
p_value = (1-distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными'
)
p-значение: 0.27348595720377333 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
purchases = np.array([212, 712])
leads = np.array([760, 2264])
p1 = purchases[0] / leads[0]
p2= purchases[1] / leads[1]
p_combined = (purchases[0] + purchases[1]) / (leads[0] + leads[1])
difference = p1 - p2
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/leads[0] + 1/leads[1]))
distr = st.norm(0, 1)
p_value = (1-distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными'
)
p-значение: 0.06571034551803678 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Разница между конверсиями оказалось значимой только на первом этапе, дальше разницы между конверсиями нет.
Общие результаты исследования такие:
Хоть по аудитории и есть не большие отклонения в целом участников достаточна для проведения теста, но тест прерывается не дав возможности отследить лайфтайм всех участников. Также тест проходит во время маркетингово события Christmas&New Year Promo перед рождественскими праздниками что сильно могло повлиять на поведения людей в группах, поэтому тест сложна считать корректным. Тесты нужна проводить с полным лайфтаймом и выбрать окно так чтобы не до, не вовремя теста не было не каких маркетинговых событий и акций.